﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using T0yK4T.Tools;
using T0yK4T.IO;
using System.Net;
using ToyChat.Server;

namespace ToyChat.Client
{
    /// <summary>
    /// The main client for ToyChat
    /// </summary>
    public class Client : ComponentBase, IClient
    {
        private CryptoConnection connection;
        private ulong totalBytesSent = 0;
        private ulong totalBytesReceived = 0;
        private Dictionary<uint, List<IClientModule>> handlers = new Dictionary<uint, List<IClientModule>>();
        private Dictionary<string, List<IClientModule>> loadedHandlers = new Dictionary<string, List<IClientModule>>();
        private IEnumerable<IAuthenticationModule> authModules = new IAuthenticationModule[0];
        private IUserInterface ui;
        private Dictionary<string, uint> nameTable = new Dictionary<string, uint>();
        private static readonly string[] excludeAssemblies = new string[] { "MongoDB.dll", "MongoDB.Bson.dll", "MongoDB.Driver.dll", "protobuf-net.dll", "Skeinfish.dll", "T0yK4T.dll" };
        
        /// <summary>
        /// This event is fired whenever the client is disconnected
        /// </summary>
        public event ClientDisconnectedHandler Disconnected;

        /// <summary>
        /// Initializes a new instance of ToyClient and sets it's settings and logger to the specified values
        /// </summary>
        /// <param name="logger">The logger to use for logging</param>
        /// <param name="ui">The user interface that instantiated this client</param>
        public Client(IUserInterface ui)
        {
            if (ui == null)
                throw new ArgumentNullException("ui");
            this.ui = ui;
            this.init();
            this.LogInformation("Client Initialized");
        }

        /// <summary>
        /// Internal initializer (Common)
        /// </summary>
        private void init()
        {
            AssemblyLoader loader = new AssemblyLoader();
            base.LogInformation("Loading Modules");
            IEnumerable<IClientModule> handlers = loader.LoadDirectory<IClientModule>(Environment.CurrentDirectory, excludeAssemblies);
            int count = 0;
            foreach (IClientModule handler in handlers)
            {
                if (handler.Types != null && handler.Types.Length > 0)
                {
                    handler.Initialize(this);
                    count++;
                    foreach (string type in handler.Types)
                    {
                        List<IClientModule> preList;
                        if (!this.loadedHandlers.TryGetValue(type, out preList))
                        {
                            preList = new List<IClientModule>();
                            this.loadedHandlers.Add(type, preList);
                        }
                        preList.Add(handler);
                    }
                }
            }
            base.LogInformation("Got {0} handlers", count);
            base.LogInformation("Getting Authentication Modules");
            IEnumerable<IAuthenticationModule> authModules = loader.LoadDirectory<IAuthenticationModule>(Environment.CurrentDirectory, excludeAssemblies);
            if (authModules != null)
            {
                this.authModules = authModules;
                //base.LogInformation("Got {0} Authentication Modules", authModules.Count());
            }
            else
            {
                base.LogCritical("Unable to get any Authentication Modules - This should never happen!");
                throw new NotSupportedException("Cannot start without any authentication modules");
            }
        }

        /// <summary>
        /// Gets the Remote Endpoint of the underlying connection
        /// </summary>
        public IPEndPoint RemoteEndpoint
        {
            get { return (IPEndPoint)this.connection.RemoteEndPoint; }
        }

        /// <summary>
        /// Attempts to connect to the ToyServer located at the specified <see cref="IPEndPoint"/>, returns true if succesful, otherwise false
        /// </summary>
        /// <param name="settings">The connection settings to use when connecting</param>
        /// <returns>True if succesful, otherwise false</returns>
        public bool ConnectTo(IConnectionSettings settings)
        {
            if (!settings.Validate())
                throw new ArgumentException("Settings could not be validated", "settings");

            bool authenticated = false;
            this.connection = new CryptoConnection(base.Logger);
            this.connection.SetFlag(CryptoConnectionFlags.ManualRead);
            this.connection.PacketReceived += new PacketReceivedDelegate(connection_PacketReceived);
            this.connection.Disconnected += new DisconnectedDelegate(connection_Disconnected);
            this.connection.ConnectTo(settings.RemoteAddress, settings.RemotePort);
            if (this.connection.Connected)
            {
                foreach (IAuthenticationModule authMod in this.authModules)
                {
                    try
                    {
                        if (authenticated = authMod.Authenticate(connection, settings, this))
                        {
                            this.connection.UnSetFlag(CryptoConnectionFlags.ManualRead);
                            break;
                        }
                    }
                    catch (Exception er)
                    {
                        base.LogException(er, System.Reflection.MethodBase.GetCurrentMethod());
                        authenticated = false;
                    }
                }
            }
            if (!authenticated)
                this.connection.Close();
            else if (this.connection.Connected)
                this.OnConnected();
            return authenticated && this.connection.Connected;
        }

        private void OnConnected()
        {
            if (this.Connected != null)
                this.Connected.Invoke(this);
        }

        /// <summary>
        /// Called from <see cref="CryptoConnection"/> when a packet has been received
        /// </summary>
        /// <param name="packet">The packet that was received</param>
        /// <param name="wireLength">The length of the actual data (before decryption) of the received packet.</param>
        void connection_PacketReceived(Packet packet, int wireLength)
        {
            this.totalBytesReceived += (ulong)wireLength;
            System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback((o) =>
            {
                List<IClientModule> hl;
                Packet p = (Packet)o;
                if (this.handlers.TryGetValue(p.TypeID, out hl))
                {
                    foreach (IClientModule handler in hl)
                    {
                        try { handler.OnPacket(p); }
                        catch (Exception er) { base.LogException(er, System.Reflection.MethodBase.GetCurrentMethod()); }
                    }
                }
            }), packet);
        }

        /// <summary>
        /// Called from <see cref="CryptoConnection"/> when it disconnected remotely
        /// </summary>
        /// <param name="which">The <see cref="CryptoConnection"/> that was disconnected</param>
        /// <param name="reason">The reason for the disconnect (can be error etc.)</param>
        void connection_Disconnected(CryptoConnection which, DisconnectReason reason)
        {
            try { which.Disconnected -= new DisconnectedDelegate(this.connection_Disconnected); }
            catch { }
            try { which.PacketReceived -= new PacketReceivedDelegate(this.connection_PacketReceived); }
            catch { }
            if (this.Disconnected != null)
                this.Disconnected.Invoke(this, reason);
        }

        /// <summary>
        /// Should be called from an <see cref="IAuthenticationModule"/>
        /// <para/>
        /// when the client has been authenticated and the server's alias list has been received
        /// </summary>
        /// <param name="aliases">The server's type aliases</param>
        public void OnAuthenticated(AliasSpecifier aliases)
        {
            if (aliases.Aliases != null)
            {
                this.handlers.Clear();
                this.nameTable = aliases.Aliases;
                foreach (KeyValuePair<string, uint> kvp in this.nameTable)
                {
                    List<IClientModule> modules;
                    if (this.loadedHandlers.TryGetValue(kvp.Key, out modules))
                    {
                        this.handlers.Add(kvp.Value, modules);
                        foreach (IClientModule module in modules)
                            module.SetTypeID(kvp.Key, kvp.Value);
                    }
                }
            }
            else
            {
                base.LogError("Got AliasSpecifier from server - but it appears to be empty - Disconnecting");
                this.Disconnect();
            }
        }

        /// <summary>
        /// Sends the specified packet to the server
        /// </summary>
        /// <param name="packet">The packet to send</param>
        public void Send(Packet packet)
        {
            if (packet == null)
                throw new ArgumentNullException("packet");
            else if (this.connection == null)
                throw new InvalidOperationException("Not connected");
            if (this.connection.Connected)
                this.totalBytesSent += (ulong)this.connection.SendPacket(packet);
        }

        /// <summary>
        /// Gets the nametable that represents the current state of the internal TypeID to name map
        /// </summary>
        public Dictionary<string, uint> IDMap { get { return this.nameTable; } }

        /// <summary>
        /// Gets a reference to the userinterface that this client was created with
        /// </summary>
        public IUserInterface UI { get { return this.ui; } }

        /// <summary>
        /// Gets a value indicating how many bytes of data this client has sent
        /// </summary>
        public ulong TotalBytesSent
        {
            get { return this.totalBytesSent; }
        }

        /// <summary>
        /// Gets a value indicating how many bytes of data this client has received
        /// </summary>
        public ulong TotalBytesReceived
        {
            get { return this.totalBytesReceived; }
        }

        /// <summary>
        /// Gets a value indicating wether or not the client considers itself as being connected to a server
        /// </summary>
        public bool IsConnected
        {
            get
            {
                if (this.connection == null)
                    return false;
                else
                    return this.connection.Connected;
            }
        }

        /// <summary>
        /// If connected, disconnects the client from the server
        /// </summary>
        public void Disconnect()
        {
            if (this.connection != null && this.connection.Connected)
            {
                this.connection.Close();
                this.connection.Disconnected -= new DisconnectedDelegate(this.connection_Disconnected);
                this.connection.PacketReceived -= new PacketReceivedDelegate(this.connection_PacketReceived);
                this.connection = null;
                if (this.Disconnected != null)
                    this.Disconnected.Invoke(this, DisconnectReason.Disconnect);
            }
        }

        /// <summary>
        /// This event is fired whenever the client considers itself as being connected to a server
        /// </summary>
        public event ClientConnectedHandler Connected;
    }
}
